package com.aynu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.aynu.gulimall.product.service.CategoryBrandRelationService;
import com.aynu.gulimall.product.vo.Catalog2Vo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.aynu.common.utils.PageUtils;
import com.aynu.common.utils.Query;

import com.aynu.gulimall.product.dao.CategoryDao;
import com.aynu.gulimall.product.entity.CategoryEntity;
import com.aynu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

//TODO SpringCache,Redis的介绍和使用
@Service("categoryService")
@Slf4j
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redisson;

    //private Map<String,Object> cache = new HashMap<>();


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {

        //因为继承ServiceImpl，而ServiceImpl有自动注入的泛型baseMapper，所有可以用baseMapper
        //1.查出所有的分类
        List<CategoryEntity> list = baseMapper.selectList(null);

        //2.组成父子的属性结构
        /*2.1找到所有的一级分类
         *   使用streamAPI的filter过滤，因为每一元素，都是categoryEntity,
         *   然后使用箭头函数()->{}
         *   最后收集成一个list集合
         *   因为只有一个参数，所以就可以省略
         * --使用map映射，把当前菜单的子分类保存进去,然后使用箭头函数，将当前菜单返回回去
         *   在收集之前改变菜单，使用递归的方法
         * --使用sorted进行排序，传入两个参数，使用箭头函数，然后返回参数1的序号减去参数2的序号
         * */
        /*List<CategoryEntity> level1Menus = list.stream().filter((categoryEntity) -> {
            return categoryEntity.getParentCid() == 0;
        }).collect(Collectors.toList());*/
        //省略写法
        /*List<CategoryEntity> level1Menus = list.stream().filter(categoryEntity ->
                categoryEntity.getParentCid() == 0
        ).collect(Collectors.toList());*/
        List<CategoryEntity> level1Menus = list.stream().filter(categoryEntity ->
                categoryEntity.getParentCid() == 0
        ).map((menu) -> {
            menu.setChildren(getChildrens(menu, list));
            return menu;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return level1Menus;
    }

    @Override
    public void removeMenusByIds(List<Long> asList) {
        //TODO //待办事务列表  1.检查当前菜单，是否被其他地方引用
        baseMapper.deleteBatchIds(asList);
    }

    /*
     * 查找catelogId的完整路径
     * 【父/子/孙/。。】
     * */
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);
        //逆序输出
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[parentPath.size()]);
    }

    /**
     * 级联更新
     *
     * --@CacheEvict:失效模式
     * 同时删除多个key
     * 1 --@Caching:同时多种缓存操作
     * 2 --@CacheEvict(value = "category",allEntries = true),删除缓存分区
     * 3 存储同一类型的数据,都可以指定成同一个分区,分区名默认就是前缀
     * @param category
     */
    //@CacheEvict(value = {"category"},key = "'getLevelCategory'")
//    @Caching(evict = {
//            @CacheEvict(value = {"category"},key = "'getLevelCategory'"),
//            @CacheEvict(value = {"category"},key = "'getCatalogJson'")
//    })
    //@CachePut//双写模式,这个没返回值,所有没办法使用
    @Override
    @Transactional
    @CacheEvict(value = "category",allEntries = true)
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        if (!StringUtils.isEmpty(category.getName())) {
            //级联更新category_Brand_Relation表
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
        }
    }

    /**
     *
     * -- @Cacheable
     * 1、代表当前方法的结果需要缓存，如果缓存中有，方法不用调用，如果缓存中没有，则将方法执行结果放入缓存
     * 2、每个需要缓存的数据我们都来指定要放到哪个名字的缓存。
     *  【缓存的分区（按照业务类型分）】
     *3、默认行为：
     *      1）、如果缓存中存在，方法不再调用
     *      2）、key默认自动生成：缓存名字: :simplekey[]（自动生成的key）
     *      3）、缓存的value的值，默认使用jdk序列化机制，将序列化后的数据存到redis
     *      4）、默认时间ttl=-1（永久）
     *   自定义：
     *      1）、指定生成的缓存使用的key ：key属性执行,接受spel表达式
     *      2）、指定存活时间：配置文件中【spring.cache.redis.time-to-live】
     *      3）、指定缓存使用的序列化机制（转化为json）
     * 4 spring-cache的不足:
     *  1) 读模式:
     *      缓存击穿:查询一个null数据.解决:缓存空数据
     *      缓存雪崩:大量并发同时查询一个正好过期的数据.解决:加锁?;
     *          默认没有加锁的,加上这个[sync = true]后,会加上synchronized,springCache在读读的时候加锁,但是在写的时候不进行加锁
     *      缓存穿透:大量的key同时过期,解决:加随机时间,可能会弄巧成拙(本来不会同时过期,加上随机时间后同时过期了),
     *          我们现在只需要加上过期时间就行了
     *  2) 写模式:
     *      1) 读写加锁
     *      2) 引入canal,感知到mysql跟新去更新缓存
     *      3) 读多写多,直接查询数据库
     *  总结:
     *      常规数据(读多写少,及时性,一致性要求不高的数据):可以使用springCache,写模式(只要缓存的数据有过期时间就行)
     *      特殊数据:特殊设计
     *  原理:
     *      cacheManager(RedisCacheManager) -> cache(RedisCache)->负责缓存的读写
     * @param level
     * @return
     */
    @Override
    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
    public List<CategoryEntity> getLevelCategory(Long level) {
        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", level));
    }

    @Override
    @Cacheable(cacheNames = {"category"},key = "#root.methodName")
    public Map<String, List<Catalog2Vo>> getCatalogJson() {
        return getCatalogFromDBNoRedis();
    }

    //TODO 堆外内存溢出：outOfDriectMemoryError
    //原因：
    //1)、springboot2.x以后默认使用lettuce作为操作的客户端，它使用netty进行网络通信
    //2)、lettuce的bug导致netty堆内存溢出，当虚拟机指定堆内存时（-Xmx100m），netty如果没有指定堆外内存，默认使用-Xmx100m
    //    可以通过-Dio.netty.maxDirectMemory进行设置
    //3)、解决方案：不能使用-Dio.netty.maxDirectMemory只去调大堆外内存
    //  1)、升级lettuce客户端
    //  2)、却换使用jedis
    //redisTemplate:
    //  是对lettuce，和jedis的在此封装，在源码中，它引入了两个连接redis的连接工厂，使用其中一个就行
    public Map<String, List<Catalog2Vo>> getCatalogJsonPrimeval() {

        /**
         * 1、空结果缓存，解决缓存穿透的问题
         * 2、设置过期时间（加随机值）：解决缓存雪崩
         * 3、加锁，解决缓存击穿的问题
         *  1)、如果使用synchronization，使用同一把锁需要锁住所有线程
         *      1）、springboot所有的组件在容器中都是单例的
         *      2）、本地锁synchronization（this）,JUC(lock)
         */

        //给缓存中放入字符串，拿出的接送字符串，调用逆转为能用的对象
        //1、加入缓存逻辑 ，缓存中存的数据是json字符串
        //JSON语言，跨平台性
        String catalogJsonRedis = redisTemplate.opsForValue().get("catalogJson");
        if (StringUtils.isEmpty(catalogJsonRedis)) {
            //2、缓存中没有，查询数据库
            return getCatalogJsonFromDBRedissonLock();
        }
        Map<String, List<Catalog2Vo>> result =
                JSON.parseObject(catalogJsonRedis, new TypeReference<Map<String, List<Catalog2Vo>>>() {});

        return result;
    }

    /**
     * 缓存里面的数据如何和数据库保持一致的
     * 缓存的数据一致：常用的方法
     * 1）、双写模式
     * 2）、失效模式
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBRedissonLock() {
        //1、占分布锁，去redis,锁得粒度，越细越快
        //所得粒度，具体缓存的时某个数据，如：11号商品-->  product—11-lock
        RLock lock = redisson.getLock("catalogJson-lock");
        //2、加锁
        lock.lock();
        try {
            return this.getCatalogFormDB();
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    //为了解决同步到redis中，出现多次查询数据库的情况
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBRedisLock() {
        //1、占分布锁，去redis
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
        if (lock) {
            log.info("获取分布式锁成功！");
            //加锁成功，执行业务
            //设置过期时间,必须和加锁同步的
            //redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map<String, List<Catalog2Vo>> catalogFormDB = null;
            try {
                catalogFormDB = this.getCatalogFormDB();
            } finally {
                //lua 脚本，保证原子性
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";

                //redis执行脚本,删除锁
                Long execute = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                        Arrays.asList("lock"), uuid);
            }

            //获取值对比+对比成功后删除锁 = 原子操作 lua脚本解锁,这个过程不是一个原子操作，舍弃
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (uuid.equals(lockValue)) {
//                //删除锁
//                redisTemplate.delete("lock");
//            }
            return catalogFormDB;
        } else {
            //加锁失败。。重试,自旋的方式
            log.info("获取锁失败，重试中...");
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonFromDBRedisLock();
        }
    }


    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBLocalLock() {

        synchronized (this) {
            String catalogJsonRedis = redisTemplate.opsForValue().get("catalogJson");
            if (!StringUtils.isEmpty(catalogJsonRedis)) {
                return JSON.parseObject(catalogJsonRedis, new TypeReference<Map<String, List<Catalog2Vo>>>() {
                });
            }
            //查询数据库封装成前端需要的数据
            Map<String, List<Catalog2Vo>> catalogs = getCatalogFormDB();
            return catalogs;
        }
    }

    private Map<String, List<Catalog2Vo>> getCatalogFormDB() {
        Map<String, List<Catalog2Vo>> catalogs = getCatalogFromDBNoRedis();
        //将查出来的数据转换为json，存入redis
        String catalogStr = JSON.toJSONString(catalogs);
        redisTemplate.opsForValue().set("catalogJson", catalogStr, 1, TimeUnit.DAYS);
        return catalogs;
    }

    private Map<String, List<Catalog2Vo>> getCatalogFromDBNoRedis() {
        System.out.println("查询数据库");
        Long level = 0L;
        log.info("查出所有分类");
        List<CategoryEntity> levelCategory = baseMapper.selectList(null);

        //过滤出来所有的一级分类
        List<CategoryEntity> catalog1 = getParentCid(levelCategory, level);

        //封装数据
        Map<String, List<Catalog2Vo>> catalogs = catalog1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

            log.info("过滤出一级分类下的二级分类");
            List<CategoryEntity> category2EntityList = getParentCid(levelCategory, v.getCatId());
            List<Catalog2Vo> catalog2Vos = null;

            if (!CollectionUtils.isEmpty(category2EntityList)) {
                //2、找到二类分类的id的三级
                catalog2Vos = category2EntityList.stream().map(catalog2 -> {
                    Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(),
                            null, catalog2.getCatId().toString(), catalog2.getName());
                    //查询三级分类的id
                    log.info("查询下级分类,三级分类");
                    List<CategoryEntity> category3EntityList = getParentCid(levelCategory, catalog2.getCatId());
                    List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;

                    if (!CollectionUtils.isEmpty(category3EntityList)) {
                        catalog3Vos = category3EntityList.stream().map(catalog3 -> {
                            return new Catalog2Vo.Catalog3Vo(catalog2.getCatId().toString(), catalog3.getCatId().toString(), catalog3.getName());
                        }).collect(Collectors.toList());
                    }
                    //封装入二级分类
                    catalog2Vo.setCatalog3List(catalog3Vos);
                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
        return catalogs;
    }

    private List<CategoryEntity> getParentCid(List<CategoryEntity> categoryEntityList, Long parentCid) {
        return categoryEntityList.stream().filter(item -> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        //收集子节点id
        paths.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0) {
            findParentPath(byId.getParentCid(), paths);
        }
        return paths;
    }

    //递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
        List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == root.getCatId();
        }).map(categoryEntity -> {
            //递归查找子菜单
            categoryEntity.setChildren(getChildrens(categoryEntity, all));
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            //菜单的排序
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return children;
    }

}